from absl import app, flags
from tornado import gen

import datetime
import functools
import json
import logging
import math

from coin.base.timestamp import get_timestamp
from coin.exchange.base.order_gateway import OrderType, OrderSide
from coin.exchange.bitflyer_v1.kr_rest.currency import BitflyerCurrency
from coin.exchange.bitflyer_v1.kr_rest.futures_product import BitflyerFuturesProduct
from coin.exchange.bitflyer_v1.kr_rest.product import BitflyerProduct
from coin.exchange.bitflyer_v1.kr_rest.product_info import get_holder_from_product
from coin.strategy.mm.base.bitflyer_strategy_base import (BitflyerMarginPositionBalancePrinter,
                                                          BitflyerStrategyBase,
                                                          run_live)
from coin.strategy.mm.bitflyer_v1.simple_mm_executor2 import (BitflyerSimpleMMExecutor)

from coin.strategy.mm.base.bookmap import BookMap

flags.DEFINE_string('config_filename', '', '')

# if you need to change the variable name to consolidate, please do so.
flags.DEFINE_string('og_config_filename', '', '')

from coin.strategy.executor.passive.util import (convert_executor_config_from_dict)


class BitflyerSfdStrategy(BitflyerStrategyBase):
  def __init__(self, execution_spec):
    self._spot_product = BitflyerProduct.FromStr('BTC-JPY')
    self._fx_product = BitflyerFuturesProduct.FromStr('BTC-JPY.PERPETUAL')
    super().__init__(products=[self._fx_product],
                     currencies=[BitflyerCurrency.FromStr('BTC'), BitflyerCurrency.FromStr('JPY')],
                     logger=logging.getLogger('BitflyerSfdStrategy'))

    self._lot_size = execution_spec['lot_size']
    self._min_pos = execution_spec['min_pos']
    self._max_pos = execution_spec['max_pos']
    # self._sfd_pct = execution_spec['sfd_pct']
    self._sfd_pct = 5.0
    # self._edge_det_pct = execution_spec['edge_det_pct']
    # self._edge_fire_pct = execution_spec['edge_fire_pct']
    self._execution_params = convert_executor_config_from_dict(self._fx_product.symbol,
                                                               execution_spec)

    self._bookmap = BookMap([self._fx_product, self._spot_product], self._logger)

    self._printer = BitflyerMarginPositionBalancePrinter(BitflyerCurrency.FromStrCurrency('JPY'),
                                                         self._fx_product,
                                                         self._bookmap,
                                                         self._logger)
    self._last_spot_price = None
    self._last_fx_price = None
    self._last_spot_time = None
    self._last_fx_time = None
    self._position = None
    self.last_fire_time = 0
    self._last_spot_bid0 = None
    self._last_spot_ask0 = None
    self._executor = None

  def prepare(self):
    # change to feed manager..
    assert self.feed_subsystem is not None
    for product in [self._spot_product, self._fx_product]:
      self.feed_subsystem.subscribe_book(product, functools.partial(self.on_book, product))
      self.feed_subsystem.subscribe_trade(product, functools.partial(self.on_trade, product))

  @gen.coroutine
  def periodic_cancel_unknown_working_orders(self):
    while True:
      if self.order_gateway.is_ready():
        self.order_gateway.cancel_unknown_working_orders()
      yield gen.sleep(30)

  def on_og_reset(self):
    self._printer.start_print(self._ioloop, 10)
    self._printer.register_order_gateway(self.order_gateway)
    self._executor = BitflyerSimpleMMExecutor(self._execution_params, self._og, self._logger)

    # Cancel all unknown working orders.
    self._ioloop.add_callback(self.periodic_cancel_unknown_working_orders)
    self._og.register_fill_callback(self.on_fill)

  def on_fill(self, fill_price, fill_qty, order):
    sfd = 100. * fill_price / self._last_spot_price - 100.
    self._logger.info('FILL %s %s %f@%f spot %.0f, SFD%.4f',
                      order.product,
                      order.order_side,
                      fill_qty,
                      fill_price,
                      self._last_spot_price,
                      sfd)
    if order.order_side == OrderSide.SELL:
      self._last_sell_fill_time = get_timestamp()
    elif order.order_side == OrderSide.BUY:
      self._last_buy_fill_time = get_timestamp()
    else:
      raise ValueError('Invalid side: %s' % str(order.order_side))

  def on_book(self, product, book):
    self._bookmap.set_book(product, book)
    if product == self._spot_product:
      self._last_spot_bid0 = book.bid0().price
      self._last_spot_ask0 = book.ask0().price
      self.on_fire()

  def on_trade(self, product, trade):
    if product == self._spot_product:
      self._last_spot_price = trade.price
      self._last_spot_time = get_timestamp()
    elif product == self._fx_product:
      self._last_fx_price = trade.price
      self._last_fx_time = get_timestamp()
    if self._last_spot_price and self._last_fx_price:
      self.on_fire()

  def get_sfd_price(self, price, ceil=False):
    if ceil:
      pricefnc = math.ceil
    else:
      pricefnc = math.floor
    return float(pricefnc(price * (100. + self._sfd_pct) / 100.))

  def get_sfd_rate(self, price):
    return 100. * price / self._last_spot_price - 100.

  def on_fire(self):
    if self._executor is None:
      return
    if not (self._last_spot_price and self._last_spot_bid0 and self._last_spot_ask0):
      return
    spot_min_price = min(self._last_spot_price, self._last_spot_bid0)
    spot_max_price = max(self._last_spot_price, self._last_spot_ask0)
    buyp = self.get_sfd_price(spot_min_price, ceil=True)
    sellp = self.get_sfd_price(spot_max_price, ceil=False)

    logging.info('B %.0f(%.4f) S %.0f(%.4f)',
                 buyp,
                 self.get_sfd_rate(buyp),
                 sellp,
                 self.get_sfd_rate(sellp))
    self._executor.manage_mm_orders(sell_price=sellp, buy_price=buyp)

  def on_fire_oldfashioned(self):
    if self._og is None:
      return
    now_ts = get_timestamp()
    spot_elapsed_ms = (now_ts - self._last_spot_time) * 1e-6
    fx_elapsed_ms = (now_ts - self._last_fx_time) * 1e-6
    sfd = 100. * self._last_fx_price / self._last_spot_price - 100.
    logging.info('spot %s(%dms ago) fx %s(%dms ago) sfd %.4f',
                 self._last_spot_price,
                 spot_elapsed_ms,
                 self._last_fx_price,
                 fx_elapsed_ms,
                 sfd)
    if max(spot_elapsed_ms, fx_elapsed_ms) > 1000:
      logging.info('skipping, too old feed')
      return
    if self._position is None:
      self._position = self._og.get_position(self._fx_product)
      return
    sell_chance = False
    buy_chance = False
    if self._position < -self._lot_size:
      if sfd < self._sfd_pct - self._edge_det_pct:
        buy_chance = True
        buy_price = float(
            math.floor(self._last_spot_price * (100. + self._sfd_pct - self._edge_fire_pct) / 100.))
        buy_qty = self._lot_size
    if sfd > self._sfd_pct + self._edge_det_pct:
      sell_chance = True
      sell_price = float(
          math.ceil(self._last_spot_price * (100. + self._sfd_pct + self._edge_fire_pct) / 100.))
      sell_qty = self._lot_size
    if sell_chance and self._position >= self._min_pos:
      self._og.submit(product=self._fx_product,
                      price=sell_price,
                      qty=sell_qty,
                      order_side=OrderSide.SELL,
                      order_type=OrderType.IOC)
      self.last_fire_time = now_ts
    elif buy_chance and self._position <= self._max_pos:
      self._og.submit(product=self._fx_product,
                      price=buy_price,
                      qty=buy_qty,
                      order_side=OrderSide.BUY,
                      order_type=OrderType.IOC)
      self.last_fire_time = now_ts
    self._position = self._og.get_position(self._fx_product)


def main(argv):
  init_og_after_min = 0
  exit_after_min = init_og_after_min + 50

  with open(flags.FLAGS.config_filename, 'r') as conf_file:
    conf = json.load(conf_file)
    strategy = BitflyerSfdStrategy(conf)
    return run_live(strategy=strategy,
                    og_config_name=flags.FLAGS.og_config_filename,
                    init_order_gateway_after=datetime.timedelta(minutes=init_og_after_min),
                    exit_after=datetime.timedelta(minutes=exit_after_min))


if __name__ == '__main__':
  app.run(main)
